#pragma once

#include <openssl/hmac.h>
#include <openssl/evp.h>
#include <openssl/hmac.h>
#include <zlib.h>

#include <cctype>
#include <cstddef>
#include <cstdlib>
#include <cstring>
#include <iomanip>
#include <sstream>
#include <stdexcept>
#include <string>
#include <random>
#include <assert.h>

#include "Packer.h"

namespace agora
{
    namespace tools
    {

        const uint32_t HMAC_LENGTH = 20;
        const uint32_t HMAC_SHA256_LENGTH = 32;
        const uint32_t SIGNATURE_LENGTH = 40;
        const uint32_t APP_ID_LENGTH = 32;
        const uint32_t UNIX_TS_LENGTH = 10;
        const uint32_t RANDOM_INT_LENGTH = 8;
        const uint32_t UID_LENGTH = 10;
        const uint32_t VERSION_LENGTH = 3;
        const std::string RECORDING_SERVICE = "ARS";
        const std::string PUBLIC_SHARING_SERVICE = "APSS";
        const std::string MEDIA_CHANNEL_SERVICE = "ACS";

        template <typename T>
        inline std::string Pack(const T &x)
        {
            Packer p;
            p << x;
            return p.pack().body();
        }

        template <typename T>
        inline void Unpack(const std::string &data, T &x)
        {
            Unpacker u(data.data(), data.length());
            u >> x;
        }

        inline bool IsUUID(const std::string &v)
        {
            if (v.length() != 32)
            {
                return false;
            }

            for (char x : v)
            {
                if (!isxdigit(x))
                {
                    return false;
                }
            }

            return true;
        }

        inline uint32_t GenerateSalt()
        {
            std::random_device r;
            return r();
        }

        // HMAC
        inline std::string HmacSign(const std::string &appCertificate,
                                    const std::string &message, uint32_t signSize)
        {
            if (appCertificate.empty())
            {
                return "";
            }
            unsigned char md[EVP_MAX_MD_SIZE];
            uint32_t md_len = 0;
            HMAC(EVP_sha1(), (const unsigned char *)appCertificate.data(),
                 appCertificate.length(), (const unsigned char *)message.data(),
                 message.length(), &md[0], &md_len);
            return std::string(reinterpret_cast<char *>(md), signSize);
        }

        inline std::string HmacSign2(const std::string &appCertificate,
                                     const std::string &message, uint32_t signSize)
        {
            if (appCertificate.empty())
            {
                return "";
            }
            unsigned char md[EVP_MAX_MD_SIZE];
            uint32_t md_len = 0;
            HMAC(EVP_sha256(), (const unsigned char *)appCertificate.data(),
                 appCertificate.length(), (const unsigned char *)message.data(),
                 message.length(), &md[0], &md_len);
            return std::string(reinterpret_cast<char *>(md), signSize);
        }

        inline std::string toupper(const std::string &in)
        {
            std::string out;
            for (char x : in)
            {
                int u = std::toupper(x);
                out.push_back((char)u);
            }
            return out;
        }

        inline std::string stringToHEX(const std::string &in)
        {
            static const char hexTable[] = "0123456789ABCDEF";

            if (in.empty())
            {
                return std::string();
            }
            std::string out(in.size() * 2, '\0');
            for (uint32_t i = 0; i < in.size(); ++i)
            {
                out[i * 2 + 0] = hexTable[(in[i] >> 4) & 0x0F];
                out[i * 2 + 1] = hexTable[(in[i]) & 0x0F];
            }
            return out;
        }

        inline std::string stringToHex(const std::string &in)
        {
            static const char hexTable[] = "0123456789abcdef";

            if (in.empty())
            {
                return std::string();
            }
            std::string out(in.size() * 2, '\0');
            for (uint32_t i = 0; i < in.size(); ++i)
            {
                out[i * 2 + 0] = hexTable[(in[i] >> 4) & 0x0F];
                out[i * 2 + 1] = hexTable[(in[i]) & 0x0F];
            }
            return out;
        }

        inline std::string hexDecode(const std::string &hex)
        {
            if (hex.length() % 2 != 0)
            {
                return "";
            }

            size_t count = hex.length() / 2;
            std::string out(count, '\0');
            for (size_t i = 0; i < count; ++i)
            {
                std::string one = hex.substr(i * 2, 2);
                out[i] = ::strtol(one.c_str(), 0, 16);
            }
            return out;
        }

        static const char base64_chars[] =
            "ABCDEFGHIJKLMNOPQRSTUVWXYZ"
            "abcdefghijklmnopqrstuvwxyz"
            "0123456789+/";

        inline char *base64_encode(const unsigned char *input, int length)
        {
            /* http://www.adp-gmbh.ch/cpp/common/base64.html */
            int i = 0, j = 0, s = 0;
            unsigned char char_array_3[3], char_array_4[4];

            int b64len = (length + 2 - ((length + 2) % 3)) * 4 / 3;
            char *b64str = new char[b64len + 1];

            while (length--)
            {
                char_array_3[i++] = *(input++);
                if (i == 3)
                {
                    char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
                    char_array_4[1] =
                        ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
                    char_array_4[2] =
                        ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
                    char_array_4[3] = char_array_3[2] & 0x3f;

                    for (i = 0; i < 4; i++)
                        b64str[s++] = base64_chars[char_array_4[i]];

                    i = 0;
                }
            }
            if (i)
            {
                for (j = i; j < 3; j++)
                    char_array_3[j] = '\0';

                char_array_4[0] = (char_array_3[0] & 0xfc) >> 2;
                char_array_4[1] =
                    ((char_array_3[0] & 0x03) << 4) + ((char_array_3[1] & 0xf0) >> 4);
                char_array_4[2] =
                    ((char_array_3[1] & 0x0f) << 2) + ((char_array_3[2] & 0xc0) >> 6);
                char_array_4[3] = char_array_3[2] & 0x3f;

                for (j = 0; j < i + 1; j++)
                    b64str[s++] = base64_chars[char_array_4[j]];

                while (i++ < 3)
                    b64str[s++] = '=';
            }
            b64str[b64len] = '\0';

            return b64str;
        }

        inline bool is_base64(unsigned char c)
        {
            return (isalnum(c) || (c == '+') || (c == '/'));
        }

        inline unsigned char *base64_decode(const char *input, int length,
                                            int *outlen)
        {
            int i = 0;
            int j = 0;
            int r = 0;
            int idx = 0;
            unsigned char char_array_4[4], char_array_3[3];
            unsigned char *output = new unsigned char[length * 3 / 4];

            while (length-- && input[idx] != '=')
            {
                // skip invalid or padding based chars
                if (!is_base64(input[idx]))
                {
                    idx++;
                    continue;
                }
                char_array_4[i++] = input[idx++];
                if (i == 4)
                {
                    for (i = 0; i < 4; i++)
                        char_array_4[i] = strchr(base64_chars, char_array_4[i]) - base64_chars;

                    char_array_3[0] =
                        (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
                    char_array_3[1] =
                        ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
                    char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

                    for (i = 0; (i < 3); i++)
                        output[r++] = char_array_3[i];
                    i = 0;
                }
            }

            if (i)
            {
                for (j = i; j < 4; j++)
                    char_array_4[j] = 0;

                for (j = 0; j < 4; j++)
                    char_array_4[j] = strchr(base64_chars, char_array_4[j]) - base64_chars;

                char_array_3[0] = (char_array_4[0] << 2) + ((char_array_4[1] & 0x30) >> 4);
                char_array_3[1] =
                    ((char_array_4[1] & 0xf) << 4) + ((char_array_4[2] & 0x3c) >> 2);
                char_array_3[2] = ((char_array_4[2] & 0x3) << 6) + char_array_4[3];

                for (j = 0; (j < i - 1); j++)
                    output[r++] = char_array_3[j];
            }

            *outlen = r;

            return output;
        }

        inline std::string base64Encode(const std::string &data)
        {
            char *r = base64_encode((const unsigned char *)data.data(), data.length());
            std::string s(r);
            delete[] r;
            return s;
        }

        inline std::string base64Decode(const std::string &data)
        {
            int length = 0;
            const unsigned char *r = base64_decode(data.data(), data.length(), &length);
            std::string s((const char *)r, (size_t)length);
            delete[] r;
            return s;
        }

        inline std::string Compress(const std::string &data,
                                    int compressionlevel = Z_DEFAULT_COMPRESSION)
        {
            z_stream zs;
            memset(&zs, 0, sizeof(zs));
            if (deflateInit(&zs, compressionlevel) != Z_OK)
            {
                return "";
            }

            zs.next_in = (Bytef *)data.data();
            zs.avail_in = data.size();

            int ret;
            char outbuffer[1500];
            std::string outstring;

            do
            {
                zs.next_out = reinterpret_cast<Bytef *>(outbuffer);
                zs.avail_out = sizeof(outbuffer);

                ret = deflate(&zs, Z_FINISH);

                if (outstring.size() < zs.total_out)
                {
                    outstring.append(outbuffer, zs.total_out - outstring.size());
                }
            } while (ret == Z_OK);

            deflateEnd(&zs);
            if (ret != Z_STREAM_END)
            {
                return "";
            }

            return outstring;
        }

        inline std::string Decompress(const std::string &data)
        {
            z_stream zs;
            memset(&zs, 0, sizeof(zs));
            if (inflateInit(&zs) != Z_OK)
            {
                return "";
            }

            zs.next_in = (Bytef *)data.data();
            zs.avail_in = data.size();

            int ret;
            char outbuffer[3000];
            std::string outstring;
            do
            {
                zs.next_out = reinterpret_cast<Bytef *>(outbuffer);
                zs.avail_out = sizeof(outbuffer);

                ret = inflate(&zs, 0);

                if (outstring.size() < zs.total_out)
                {
                    outstring.append(outbuffer, zs.total_out - outstring.size());
                }

            } while (ret == Z_OK);

            inflateEnd(&zs);
            if (ret != Z_STREAM_END)
            {
                return "";
            }

            return outstring;
        }
    }
}
